今天的內容可以用這張圖來表示:
可以看到內容涵括 Operation Name, Aliases, Fragment, Arguments, Variables 5 個 query 技巧,今天不只教 Client 端的 Query 也教你如何實作 Server 的 Schema 與 Resolver。
在開始前依然做個觀念釐清, Operation Name, Aliases, Fragment 三個純為 query 技巧 ; 而 Arguments 與 Variables 兩個 query 技巧則需要 Schema 設計的參與。
不過因為 Schema 若是太簡單也沒有用到 進階 query 的需要,因此我先從較複雜的 Arguments & Variables 開始講起:
在 RESTful API,你需要透過 query parameter 或是 URL segments 傳遞參數來獲取不同的資料,並且常常需要很多支 API 才能獲得想要的資料,更別提一堆傳入的參數要如何管理。
在 GraphQL 為了能夠優雅地達到「一次解決」的特性,允許每一個 field 都可以傳入參數,即使是 Scalar field
。
先看看在 query 中如何使用 Arguments ,我將會使用新增的 Query field user
(功能為得到特定 user) 做舉例:
query {
# 傳入 Argument "Fong" (Argument for Object Type)
user(name: "Fong") {
id
name
# 傳入 Argument METRE (Argument for Scalar Type),
# 此 field 回傳 FLOAT type
height(unit: FOOT)
# 傳入 Argument POUND (Argument for Scalar Type),
# 此 field 回傳 FLOAT type
weight(unit: POUND)
}
}
{
"data": {
"user": {
"id": 1,
"name": "Fong",
"height": 5.741469816272966,
"weight": 154.3235835294143
}
}
}
也可以測試看看如果 height
, weight
不帶參數會有什麼結果,可以到之前的範例(傳送門)練習看,或是直接點開圖。
query 是不是很簡單 ? 那就讓我們來講解如何實現吧:
# Enum Type 為一種特殊的 Scalar Type ,使用時只能出現裡面有定義到的值且不需要加引號
# 進入 JavaSript 中使用時,會轉為 String 格式
"""
高度單位
"""
enum HeightUnit {
"公尺"
METRE
"公分"
CENTIMETRE
"英尺 (1 英尺 = 30.48 公分)"
FOOT
}
"""
重量單位
"""
enum WeightUnit {
"公斤"
KILOGRAM
"公克"
GRAM
"磅 (1 磅 = 0.45359237 公斤)"
POUND
}
type User {
...
"身高 (預設為 CENTIMETRE)"
height(unit: HeightUnit = CENTIMETRE): Float
"體重 (預設為 KILOGRAM)""
weight(unit: WeightUnit = KILOGRAM): Float
}
type Query {
...
"取得特定 user (name 為必填)"
user(name: String!): User
}
const resolverMap = {
Query: {
...
// 對應到 Schema 的 Query.user
user: (root, args, context) => {
// 取出參數。因為 name 為 non-null 故一定會有值。
const { name } = args;
return users.find(user => user.name === name);
}
},
User: {
...,
// 對應到 Schema 的 User.height
height: (parent, args) => {
const { unit } = args;
// 可注意到 Enum type 進到 javascript 就變成了 String 格式
// 另外支援 default 值 CENTIMETRE
if (!unit || unit === "CENTIMETRE") return parent.height;
else if (unit === "METRE") return parent.height / 100;
else if (unit === "FOOT") return parent.height / 30.48;
throw new Error(`Height unit "${unit}" not supported.`);
},
// 對應到 Schema 的 User.weight
weight: (parent, args, context) => {
const { unit } = args;
// 支援 default 值 KILOGRAM
if (!unit || unit === "KILOGRAM") return parent.weight;
else if (unit === "GRAM") return parent.weight * 100;
else if (unit === "POUND") return parent.weight / 0.45359237;
throw new Error(`Weight unit "${unit}" not supported.`);
}
},
}
看完 code 後,我們可以觀察到:
!
來決定參數為必填 (Non-Null) 或選填 (Nullable)Input Object Type
,這在明天的 Mutation 會有更詳細的講解user: (root, args, context)
使用 root
而非 parent
,其實兩者的意思差不多,只是因為 Query 已經是最外層的 field ,所以他的 parent 就是 root
,而這個 root 的值是可以透過初始化 Apollo Server 時指定進去的 (預設為 {}
)。隨著 Schema 越來越龐大, query 也會越來越複雜,一筆 query 可能會需要十幾個參數輸入,讓 query 十分難以管理,於是讓我們來介紹 Variables 功能!
# 因為 user 的 argument name 為必填 `!`,所以在參數宣告列上也要加上 `!`
query ($name: String!) {
user(name: $name) {
id
name
},
}
加上 Variables 區域 (json 格式):
{
"name": "Fong"
}
{
"data": {
"user": {
"id": 1,
"name": "Fong",
}
}
}
可以發現,加入 Variables 可以減少 query 管理參數的複雜度,可以僅藉由 variables 的變化來決定 query 的結果。
不過要注意,使用 variable 的話一定要先宣告,且使用時要前面要加上 $
。
接下介紹的純為 Client Side 的 Query 技巧,不涉及 Server Schema 的程式。
在前面的 Query 的例子中,我們都只使用簡寫的格式: query {}
來索取資料,但其實完整的寫法應該是:
query OperationName {
...
}
或是 Mutation (明天會再介紹)
mutation OperationName {
...
}
加上 Operation Name 有幾點好處:
query MyBasicInfo {
me {
id
name
age
}
}
{
"data": {
"me": {
"id": 1,
"name": "Fong",
"age": 23
}
}
}
可看到回傳的資料並無不同,但相信我,在做效能與錯誤追蹤時相當有用 !
經驗談: 因為我們有使用 Apollo Engine 來紀錄分析前端送來的每個 query,若沒加上 Operation 就只會得到一堆 query
, 這樣有記錄跟沒記錄一樣!
講完 Operation 外層的 Operation Name,Operation 內層的 query 也可以加上命名 (別名)!
假如你今天需要在一筆 Query 中抓兩筆特定 user 的資料,因為同筆 Query 內不能有相同的 field Name 不然回傳的 json 會搞混。這時 Aliases 就可以協助撞名的困擾:
query UserData($name1: String!, $name2: String!, $name3: String!) {
user1: user(name: $name1) {
id
name
},
user2: user(name: $name2) {
id
name
},
user3: user(name: $name3) {
id
name
},
}
加上 variables
{
"name1": "Fong",
"name2": "Kevin",
"name3": "Mary"
}
{
"data": {
"user1": {
"id": "1",
"name": "Fong"
},
"user2": {
"id": "2",
"name": "Kevin"
},
"user3": {
"id": "3",
"name": "Mary"
}
}
}
可以注意到 data 裡面的 field 名稱會變成 aliases 的名稱。
有沒已經開始覺得上一支 query 已經有點複雜且充斥著 duplicate code 了呢?此時 Fragments
就可以幫助你 reuse 重複的 Code 並增加可讀性。
query {
user1: user(name: "Kevin") {
...userData
}
user2: user(name: "Mary") {
...userData
}
}
fragment userData on User {
id
name
}
{
"data": {
"user1": {
"id": "2",
"name": "Kevin"
},
"user2": {
"id": "3",
"name": "Mary"
}
}
}
Fragment 在前端是非常好用的一樣工具,大大減少 duplicate code 以外也讓 query 管理更加方便 !
不過要注意的是, query 的 Fragment 與 schema 的 type 不同, Fragment 只存在於當下的 query , 千萬不要以為使用過一次有定義過下次就可以不定義就直接用,會吃上 Error 的 !
今天的內容不難但東西有點多,希望大家可以花時間消化一下,在這邊提供幾個題目給大家練練。
若懶得打開自己的 project 也可以到第三天的 GraphQL Playground Demo 直接練習 (傳送門)
明天就會開始教 GraphQL 的三大支柱之一的 Mutation ,請敬請期待!
給個小建議~
這篇文章中的"Variables 範例"段落裡
"加上 Variables 區域"
這個步驟....大概額外花了快半小時在搞懂這句話在說什麼吧@@"
估狗了很久, 才終於看到有另一篇文章的截圖說明所謂的"Variables區域"是指哪裡...
偏偏這段落的這一步沒有附上結果截圖, 對於初學讀者來說就是個突然插入的新名詞
丈二金剛莫不著頭緒...Orz|||
可能之前在解說的時候有帶過, 但到了這邊已經完全沒印象了
起了local後的環境像這樣
默默縮在左下角, 很難注意到這東西XD
也或許是小弟駑鈍, 算是個案, 只是想留個言讓之後看到的人可以避免一樣的情況
還是很感謝樓主寫了這系列的文章, 造福世人!!
繼續往下學習!!
tacodrem 謝謝
謝謝提醒!不好意思當初要趕 30 天死線所以沒有辦法關注到很多細節!下次若有類似問題可以直接留言詢問喔~